(function($){
    var defaults = {
        selector: '> *',
        visible: { opacity: 1, transform: 'scale(1)' },
        hidden: { opacity: 0, transform: 'scale(0.001)' }
    };

    var expando = $.expando + 'mm';

    var $fix = function (position) {
        var positions = [];

        // Positions are relative to parent
        this.parent().css({position: 'relative'});

        // Find all positions without altering elements
        this.each(function () {
            positions.push($(this).position());
        });

        // Set absolute positions
        this.each(function (i) {
            $(this).css($.extend({position: 'absolute'}, positions[i]));
        });

        return this;
    };

    var $unfix = function () {
        return this.css({position: '', top: '', left: ''});
    };

    var $transition = function (props, options) {
        var defaults = {
            easing: 'linear',
            duration: 400
        };

        options = $.extend(
            {}, defaults, options
        );

        var effect = [
            'all',
            options.duration + 'ms',
            options.easing
        ].join(' ');

        props = $.extend(
            {transition: effect},
            props
        );

        var callback = function () {
            $(this).css('transition', '');
            $(this).dequeue();
        };

        return this.queue(function () {
            setTimeout($.proxy(callback, this), options.duration);
            $(this).css(props);
        });
    };

    var $redraw = function () {
        return this.each(function () {
            var redraw = this.offsetTop;
        });
    };

    var difference = function ($nodesA, $nodesB) {
        return $nodesA.filter(function () {
            var nodeA = this;

            return !$nodesB.filter(function () {
                return this[expando] == nodeA[expando];
            })[0];
        });
    };

    var intersect = function ($nodesA, $nodesB) {
        return $nodesA.filter(function () {
            var nodeA = this;

            return $nodesB.filter(function () {
                return this[expando] == nodeA[expando];
            })[0];
        });
    };

    var magicMove = function (options, callback) {
        if (typeof options === 'function') {
            callback = options;
            options  = {};
        }

        options = $.extend({}, defaults, options);

        var promises    = [];
        var $el         = $(this);
        var $nodes      = $el.find(options.selector);
        var $clone      = $el.clone(true, true);
        var $cloneNodes = $clone.find(options.selector);

        // Set a ID to each element, so we match up
        // all the $nodes and $cloneNodes
        $nodes.each(function (index) {
            this[expando] = $cloneNodes[index][expando] = index;
        });

        $clone.css({
            position: 'absolute',
            left:     '-1000px',
            top:      '-1000px'
        }).appendTo('body');

        // Apply transformation to clone
        callback.call($clone, $clone, options);

        // Refresh cloned nodes
        $tNodes = $clone.find(options.selector);

        var $added   = difference($tNodes, $nodes);
        var $removed = difference($nodes, $tNodes);
        var $changed = intersect($tNodes, $nodes);

        // Compare nodes in $cloneNodes to nodes in $nodes
        // If node doesn't exist, create it, and animate
        // If node has moved, animate move
        // If node has been removed, fade it out

        $fix.call($nodes);

        $changed.each(function () {
            var $node  = $($nodes[this[expando]]);
            var $clone = $(this);
            var props = $clone.position();

            // If display state has changed
            if ($clone.is(':visible') && $node.is(':hidden')) {
                $.extend(props, options.visible);

            } else if ($clone.is(':hidden') && $node.is(':visible')) {
                $.extend(props, options.hidden);
                $node.promise().done(function () {
                    $node.hide();
                });
            }

            $transition.call($node, props, options);
            promises.push($node.promise());
        });

        $added.each(function () {
            var $clone = $(this);
            var $pos   = $nodes.eq($clone.index());
            var $node  = $fix.call($clone).clone();

            $node.css(options.hidden);
            $node.insertBefore($pos);
            $redraw.call($node);

            $transition.call($node, options.visible, options);
            promises.push($node.promise());
        });

        $removed.each(function () {
            var $node  = $($nodes[this[expando]]);

            $transition.call($node, options.hidden, options);
            $node.promise().done(function () {
                $node.remove();
            });
            promises.push($node.promise());
        });

        $clone.remove();

        var promise = $.when.apply($, promises);

        return promise.done(function () {
            // Remove 'absolute' styles
            $unfix.call($el.find(options.selector));
        });
    };

    $.fn.magicMove = function (options, callback) {
        var $el = this;

        // Throttle animations, so they happen sequentially
        return $el.queue(function () {
            var promise = magicMove.call($el, options, callback);

            promise.done(function () {
                $el.dequeue();
            });
        });
    };
})(jQuery);